The 1/n family#

We play with the \(1/n\) portfolio. We start with a vanilla implementation using daily rebalancing. Every portfolio should be the solution of a convex optimization problem, see https://www.linkedin.com/pulse/stock-picking-convex-programs-thomas-schmelzer. We do that and show methods to construct the portfolio with

  • the minimization of the Euclidean norm of the weights.

  • the minimization of the \(\infty\) norm of the weights.

  • and the maximization of the Entropy of the weights.

  • the minimization of the tracking error to an \(1/n\) portfolio.

We also play with sparse updates, e.g. rather than rebalancing daily, we act only once the deviation of our drifted portfolio got too large from the target \(1/n\) portfolio.

This problem has been discussed https://www.linkedin.com/feed/update/urn:li:activity:7149432321388064768/

import pandas as pd
import numpy as np

from cvx.simulator import Builder
# load prices from flat csv file
prices = pd.read_csv("data/stock-prices.csv", header=0, index_col=0, parse_dates=True)
# Implement the 1/n portfolio using the Builder
builder = Builder(prices=prices, initial_aum=1e6)

for _, state in builder:
    assets = state.assets
    n = len(assets)
    builder.weights = np.ones(n)/n
    # it's important to also set the aum after setting the weights
    # Here one could apply trading costs
    # Access via state.trades, etc.
    builder.aum = state.aum

portfolio = builder.build()
portfolio.snapshot(aggregate=True)

With cvxpy#

Formulating the problem above as a convex program is most useful when additional constraints have to be reflected. It also helps to link the 1/n portfolio to Tikhonov regularization and interpret its solution as a cornercase for more complex portfolios we are building

import cvxpy as cp

Minimization of the Euclidean norm#

We minimize the Euclidean norm of the weight vector. Same results as above but with opten door to the world of convex paradise.

builder = Builder(prices=prices, initial_aum=1e6)

for _, state in builder:
    assets = state.assets
    n = len(assets)
    weights = cp.Variable(n)
    objective = cp.norm(weights, 2)
    constraints = [weights >= 0, cp.sum(weights) == 1]
    # we are using the new CLARABEL solver
    cp.Problem(objective=cp.Minimize(objective), constraints=constraints).solve(solver=cp.CLARABEL)
    # update weights & aum as before
    builder.weights = weights.value
    builder.aum = state.aum

portfolio = builder.build()
portfolio.snapshot(aggregate=True)

Minimization of the \(\infty\) norm#

Based on an idea by Vladimir Markov

builder = Builder(prices=prices, initial_aum=1e6)

for _, state in builder:
    assets = state.assets
    n = len(assets)
    weights = cp.Variable(n)
    objective = cp.norm_inf(weights)
    constraints = [weights >= 0, cp.sum(weights) == 1]
    # we are using the new CLARABEL solver
    cp.Problem(objective=cp.Minimize(objective), constraints=constraints).solve(solver=cp.CLARABEL)
    # update weights & aum as before
    builder.weights = weights.value
    builder.aum = state.aum

portfolio = builder.build()
portfolio.snapshot(aggreagate=True)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[7], line 16
     13     builder.aum = state.aum
     15 portfolio = builder.build()
---> 16 portfolio.snapshot(aggreagate=True)

TypeError: Portfolio.snapshot() got an unexpected keyword argument 'aggreagate'

Maximization of the entropy#

One can also maximize the entropy to arrive at the same result

builder = Builder(prices=prices, initial_aum=1e6)

for _, state in builder:
    assets = state.assets
    n = len(assets)
    weights = cp.Variable(n)
    objective = cp.sum(cp.entr(weights))
    constraints = [weights >= 0, cp.sum(weights) == 1]
    cp.Problem(objective=cp.Maximize(objective), constraints=constraints).solve(solver=cp.CLARABEL)
    # update weights & aum as before
    builder.weights = weights.value
    builder.aum = state.aum

portfolio = builder.build()
portfolio.snapshot()

Minimization of the tracking error#

builder = Builder(prices=prices, initial_aum=1e6)

for _, state in builder:
    assets = state.assets
    n = len(assets)
    weights = cp.Variable(n)
    objective = cp.norm(weights - np.ones(n)/n, 2)
    constraints = [weights >= 0, cp.sum(weights) == 1]
    cp.Problem(objective=cp.Minimize(objective), constraints=constraints).solve(solver=cp.CLARABEL)
    # update weights & aum as before
    builder.weights = weights.value
    builder.aum = state.aum

portfolio = builder.build()
portfolio.snapshot(aggregate=True)

With sparse updates#

In practice we do not want to rebalance the portfolio every day. We tolerate our portfolio is not an exact \(1/n\) portfolio. We may expect slightly weaker results

builder = Builder(prices=prices, initial_aum=1e6)

for _, state in builder:
    # assets currently alive, e.g. with a valid price
    assets = state.assets
    # number of assets currently alive
    n = len(assets)

    # Assets may drop out...
    target = np.ones(n) / n

    # the drifted weights for all valid assets
    drifted = state.weights[assets].fillna(0.0)

    # the delta is the sum of absolute weight changes
    delta = (target - drifted).abs().sum()
    
    if delta > 0.20:
        # update the weights of the portfolio, e.g.
        # rebalance it and set it all back to 1/n
        builder.weights = target
    else:
        # forward-fill the position 
        builder.position = state.position
        # or
        # forward-fill the weights
        # builder.weights = drifted
        # or
        # forward-filil the cashposition
        # builider.cashposition = state.cashposition

    # update the aum. Before you do that, you have the chance to correct it 
    # using your estimated trading costs, etc.
    builder.aum = state.aum


portfolio = builder.build()
portfolio.snapshot(aggregate=True)